﻿using System;
using System.Threading.Tasks;
using System.Transactions;

namespace ServerUtils
{
    public abstract class BaseRepository<TDbContext> : LogGenerator where TDbContext : IDbContext, new()
    {
        private MRInvoker invoker;
        private MRInvokerWithTransaction transactionalInvoker;

        public event EventHandler<InvokerErrorEventArgs> Error;

        public BaseRepository(
            int retryInterval, 
            int maxRetry, 
            IsolationLevel isolationLevel = IsolationLevel.ReadCommitted,
            TransactionScopeOption transactionScopeOption = TransactionScopeOption.Required)
        {
            this.invoker = new MRInvoker(retryInterval, maxRetry);
            this.invoker.Error += this.InvokerError;

            this.transactionalInvoker = new MRInvokerWithTransaction(retryInterval, maxRetry, isolationLevel, transactionScopeOption);
            this.transactionalInvoker.Error += this.InvokerError;
        }

        public void Invoke(Action<TDbContext> action)
        {
            this.invoker.Invoke(() => this.InvokeAction(action));
        }

        public async Task InvokeAsync(Func<TDbContext, Task> asyncAction)
        {
            await this.invoker.InvokeAsync(async () => await this.InvokeActionAsync(asyncAction));
        }

        public TResult Invoke<TResult>(Func<TDbContext, TResult> func)
        {
            return this.invoker.Invoke(() => { return this.InvokeFunction(func); });
        }

        public async Task<TResult> InvokeAsync<TResult>(Func<TDbContext, Task<TResult>> func)
        {
            return await this.invoker.InvokeAsync(async () => { return await this.InvokeFunctionAsync(func); });
        }

        public void InvokeWithTransaction(Action<TDbContext> action)
        {
            this.transactionalInvoker.Invoke(() => this.InvokeAction(action));
        }

        public async Task InvokeAsyncWithTransaction(Func<TDbContext, Task> asyncAction)
        {
            await this.transactionalInvoker.InvokeAsync(async () => await this.InvokeActionAsync(asyncAction));
        }

        public TResult InvokeWithTransaction<TResult>(Func<TDbContext, ITransactionalOperation<TResult>> func)
        {
            return this.transactionalInvoker.Invoke(() => { return this.InvokeFunction(func); });
        }

        public async Task<TResult> InvokeAsyncWithTransaction<TResult>(Func<TDbContext, Task<ITransactionalOperation<TResult>>> func)
        {
            return await this.transactionalInvoker.InvokeAsync(async () => { return await this.InvokeFunctionAsync(func); });
        }

        protected virtual void OnError(InvokerErrorEventArgs e)
        {
            var handler = this.Error;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        protected virtual TDbContext CreateDbContext()
        {
            return new TDbContext();
        }

        private void InvokerError(object sender, InvokerErrorEventArgs e)
        {
            this.OnError(e);
        }

        private void InvokeAction(Action<TDbContext> action)
        {
            using (var db = this.CreateDbContext())
            {
                db.Open();
                action.Invoke(db);
            }
        }

        private async Task InvokeActionAsync(Func<TDbContext, Task> asyncAction)
        {
            using (var db = this.CreateDbContext())
            {
                db.Open();
                await asyncAction.Invoke(db);
            }
        }

        private TResult InvokeFunction<TResult>(Func<TDbContext, TResult> func)
        {
            using (var db = this.CreateDbContext())
            {
                db.Open();
                return func.Invoke(db);
            }
        }

        private async Task<TResult> InvokeFunctionAsync<TResult>(Func<TDbContext, Task<TResult>> func)
        {
            using (var db = this.CreateDbContext())
            {
                db.Open();
                return await func.Invoke(db);
            }
        }
    }
}
